﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using theeg.GeometryWpf;
using theeg.MatrixWpf.Models;
using theeg.MatrixWpf.Views;

namespace theeg.MatrixWpf
{
    public partial class MatrixControl
    {
        public Size CellSize
        {
            get { return (Size)GetValue(CellSizeProperty); }
            set { SetValue(CellSizeProperty, value); }
        }

        public static readonly DependencyProperty CellSizeProperty =
            DependencyProperty.Register("CellSize", typeof(Size), typeof(MatrixControl), new PropertyMetadata(new Size(24, 16)));

        public Element[] Elements
        {
            get { return (Element[])GetValue(ElementsProperty); }
            set { SetValue(ElementsProperty, value); }
        }

        public static readonly DependencyProperty ElementsProperty =
            DependencyProperty.Register("Elements", typeof(Element[]), typeof(MatrixControl), new PropertyMetadata());

        private MatrixTransform _transformBackup = new MatrixTransform();
        private int _skipUpdateObjectsInScopeCount;
        private double _verticalOffset;
        private double _horizontalOffset;
        private const double ScrollbarMinimum = -1;
        private const double ScrollbarMaximum = 1;
        private double _verticalScrollValue;
        private double _horizontalScrollValue;

        private readonly IList<Element> _visibleElementsHorizontal = new List<Element>();
        private readonly IList<Element> _visibleElementsVertical = new List<Element>();

        private readonly SparseMatrix<int, Cell> _cells = new SparseMatrix<int, Cell>();

        public MatrixControl()
        {
            InitializeComponent();
        }

        private void UpdateRenderTransform()
        {
            if (MatrixPanel == null)
            {
                return;
            }
            var boundingArea = GetBoundingBox();

            double w = MatrixPanel.Width = boundingArea.Width;
            double h = MatrixPanel.Height = boundingArea.Height;

            Matrix renderMatrix = new Matrix
            {
                M11 = 1,
                M22 = 1,
                OffsetX = (w - (boundingArea.L + boundingArea.R)) * 0.5,
                OffsetY = (h - (boundingArea.B + boundingArea.T)) * 0.5
            };
            _transformBackup = new MatrixTransform
            {
                Matrix = renderMatrix
            };
            MatrixPanel.RenderTransform = _transformBackup;
        }

        internal ComputationalRectangle GetBoundingBox()
        {
            return new ComputationalRectangle(0, 0, 500, 500);
        }

        private void EnterSuppressUpdate()
        {
            _skipUpdateObjectsInScopeCount++;
        }

        private void LeaveSuppressUpdate()
        {
            _skipUpdateObjectsInScopeCount = Math.Max(0, _skipUpdateObjectsInScopeCount - 1);
        }

        private void ScrollToVerticalOffset(double p)
        {
            _verticalOffset = p;
            ApplyTranslateTranform(_verticalOffset, _horizontalOffset);
            UpdateViewport();
        }

        private void ScrollToHorizontalOffset(double p)
        {
            _horizontalOffset = p;
            ApplyTranslateTranform(_verticalOffset, _horizontalOffset);
            UpdateViewport();
        }

        private double GetScrollableHeight(ComputationalRectangle boundingArea)
        {
            return 0.5 * ((boundingArea.B - boundingArea.T) + Viewport.ActualHeight);
        }

        private double GetScrollableWidth(ComputationalRectangle boundingArea)
        {
            return 0.5 * ((boundingArea.R - boundingArea.L) + Viewport.ActualWidth);
        }

        private void ApplyTranslateTranform(double verticalOffset, double horizontalOffset)
        {
            var oldmatrix = _transformBackup.Matrix;
            var newmatrix = new Matrix(
                oldmatrix.M11, oldmatrix.M12,
                oldmatrix.M21, oldmatrix.M22,
                oldmatrix.OffsetX - horizontalOffset, oldmatrix.OffsetY - verticalOffset);

            MatrixPanel.RenderTransform = new MatrixTransform
            {
                Matrix = newmatrix
            };
        }

        private void UpdateViewport()
        {
            if (_skipUpdateObjectsInScopeCount != 0 || Viewport == null) return;
            var cellSize = CellSize;
            if (cellSize.Width < 1 || cellSize.Height < 1) return;
            var boundingArea = GetBoundingBox();
            Point upperLeft = new Point(0.5 * (boundingArea.L + boundingArea.R) - (0.5 * Viewport.ActualWidth - _horizontalOffset),
                                  0.5 * (boundingArea.B + boundingArea.T) - (0.5 * Viewport.ActualHeight - _verticalOffset));
            double w = Math.Ceiling(Viewport.ActualWidth / cellSize.Width);
            double h = Math.Ceiling(Viewport.ActualHeight / cellSize.Height);
            upperLeft = new Point(Math.Floor((upperLeft.X) / cellSize.Width), Math.Floor((upperLeft.Y) / cellSize.Height));

            var columnsInScope = _visibleElementsHorizontal.Skip((int)upperLeft.X).Take((int)w).ToArray();
            var rowsInScope = _visibleElementsVertical.Skip((int)upperLeft.Y).Take((int)w);

            var objectsInScope = new HashSet<Cell>();


            var alreadyVisible = new HashSet<Cell>();
            var children = MatrixPanel.Children;

            //remove objects that are no longer visible
            for (int i = children.Count - 1; i > -1; i--)
            {
                UIElement child = children[i];
                if (child is CellPresenter)
                {
                    Cell cell = ((CellPresenter)child).Cell;
                    if (!objectsInScope.Contains(cell))
                    {
                        children.RemoveAt(i);
                    }
                    else
                    {
                        alreadyVisible.Add(cell);
                    }
                }
            }
            //add objects, which became visible
            foreach (Cell obj in objectsInScope)
            {
                if (alreadyVisible.Contains(obj))
                {
                    continue;
                }
                children.Add(new CellPresenter
                {
                    Cell = obj
                });
            }
        }

        private void CenterTo(double x, double y)
        {
            if (Viewport != null)
            {
                var boundingArea = GetBoundingBox();

                double scrollableWidth = GetScrollableWidth(boundingArea);
                double scrollableHeight = GetScrollableHeight(boundingArea);
                if (double.IsNaN(scrollableHeight) || double.IsNaN(scrollableWidth)) return;

                _horizontalScrollValue = Math.Max(-1, Math.Min(1, ((x - boundingArea.L) - 0.5 * (boundingArea.Width)) / scrollableWidth));
                _verticalScrollValue = Math.Max(-1, Math.Min(1, ((y - boundingArea.T) - 0.5 * (boundingArea.Height)) / scrollableHeight));
                if (HorizontalScrollBar != null)
                {
                    HorizontalScrollBar.Value = _horizontalScrollValue;
                }
                if (VerticalScrollBar != null)
                {
                    VerticalScrollBar.Value = _verticalScrollValue;
                }

                EnterSuppressUpdate();
                ScrollToHorizontalOffset(_horizontalScrollValue * scrollableWidth);
                LeaveSuppressUpdate();
                ScrollToVerticalOffset(_verticalScrollValue * scrollableHeight);
            }
        }

        private void ViewportSizeChanged(object sender, SizeChangedEventArgs e)
        {
            EnterSuppressUpdate();
            UpdateRenderTransform();
            ScrollToHorizontalOffset(_horizontalOffset);
            LeaveSuppressUpdate();
            ScrollToVerticalOffset(_verticalOffset);
        }

        private bool _moveBarrierCrossed;
        private Point _lastMouseDown;
        private Point _lastMouseDownCenter;

        private void ViewportPreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left)
            {
                _moveBarrierCrossed = false;
                _lastMouseDown = e.GetPosition(Viewport);
                _lastMouseDownCenter = MatrixPanel.PointFromScreen(
                    Viewport.PointToScreen(new Point(Viewport.ActualWidth * 0.5, Viewport.ActualHeight * 0.5)));
            }
        }

        private void ViewportMouseMove(object sender, MouseEventArgs e)
        {
            if (Mouse.LeftButton == MouseButtonState.Pressed)
            {
                var currentPoint = e.GetPosition(Viewport);
                var dx = (_lastMouseDown.X - currentPoint.X);
                var dy = (_lastMouseDown.Y - currentPoint.Y);
                if (!_moveBarrierCrossed)
                {
                    if (dx * dx + dy * dy > 200)
                    {
                        _moveBarrierCrossed = true;
                    }
                }
                if (_moveBarrierCrossed)
                {
                    CenterTo(_lastMouseDownCenter.X + dx, _lastMouseDownCenter.Y + dy);
                }
            }
        }

        protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
        {
            if (Viewport != null && Viewport.IsVisible)
            {
                var boundingArea = GetBoundingBox();
                if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0
                    || Mouse.LeftButton == MouseButtonState.Pressed)
                {
                    ScrollByMouseWheelHorizontal(e, boundingArea);
                }
                else
                {
                    ScrollByMouseWheelVertical(e, boundingArea);
                }
            }
            e.Handled = true;
        }

        private void ScrollByMouseWheelVertical(MouseWheelEventArgs e, ComputationalRectangle boundingArea)
        {
            double scrollableHeight = GetScrollableHeight(boundingArea);
            //scroll by 60 device pixels
            double delta = (60.0 / scrollableHeight);
            if (e.Delta > 0)
            {
                if (_verticalScrollValue > ScrollbarMinimum)
                {
                    _verticalScrollValue = Math.Max(ScrollbarMinimum, _verticalScrollValue - delta);
                }
            }
            else
            {
                if (_verticalScrollValue < ScrollbarMaximum)
                {
                    _verticalScrollValue = Math.Min(ScrollbarMaximum, _verticalScrollValue + delta);
                }
            }
            if (double.IsNaN(_verticalScrollValue))
            {
                return;
            }
            VerticalScrollBar.Value = _verticalScrollValue;
            ScrollToVerticalOffset(_verticalScrollValue * scrollableHeight);
        }

        private void ScrollByMouseWheelHorizontal(MouseWheelEventArgs e, ComputationalRectangle boundingArea)
        {
            double scrollableWidth = GetScrollableWidth(boundingArea);
            //scroll by 60 device pixels
            double delta = (60.0 / scrollableWidth);
            if (e.Delta < 0)
            {
                if (_horizontalScrollValue > ScrollbarMinimum)
                {
                    _horizontalScrollValue = Math.Max(ScrollbarMinimum, _horizontalScrollValue - delta);
                }
            }
            else
            {
                if (_horizontalScrollValue < ScrollbarMaximum)
                {
                    _horizontalScrollValue = Math.Min(ScrollbarMaximum, _horizontalScrollValue + delta);
                }
            }
            if (double.IsNaN(_verticalScrollValue))
            {
                return;
            }
            HorizontalScrollBar.Value = _horizontalScrollValue;
            ScrollToHorizontalOffset(_horizontalScrollValue * scrollableWidth);
        }
    }
}
